Skip to content

feat: consolidate caches under ~/.config/tokscale/cache, render TUI cache eagerly, add --write-cache opt-in for light mode#473

Merged
junhoyeo merged 16 commits intomainfrom
feat/unified-cache-dir
Apr 27, 2026
Merged

feat: consolidate caches under ~/.config/tokscale/cache, render TUI cache eagerly, add --write-cache opt-in for light mode#473
junhoyeo merged 16 commits intomainfrom
feat/unified-cache-dir

Conversation

@junhoyeo
Copy link
Copy Markdown
Owner

@junhoyeo junhoyeo commented Apr 26, 2026

Consolidates tokscale state to a single root, removes the eager-refresh penalty on TUI launch, and adds an opt-in for warming the TUI cache from --light reports.

Closes #470, #471, #472.

Changes

~/.config/tokscale/cache/ is the new home for all caches (#470)

Tokscale state was scattered across up to four directories per platform. On macOS specifically the source-message bincode and pricing JSON lived under ~/Library/Caches/tokscale while the TUI display cache lived under ~/.cache/tokscale, and Wrapped image/font caches were in a third place. The new defaultClients setting from #464 made this split visible: users who set TOKSCALE_CONFIG_DIR for an isolated profile got a hermetic config root but the caches still leaked from the host paths.

Every cache now resolves under <config_dir>/cache:

  • tui-data-cache.json
  • source-message-cache.bin + .lock
  • pricing-litellm.json + pricing-openrouter.json
  • opencode-migration.json
  • fonts/ and images/ (Wrapped)

TOKSCALE_CONFIG_DIR controls everything in one shot. rm -rf ~/.config/tokscale/cache wipes only regenerable state.

Migration is read-side, not destructive: each cache module probes the new path first, then falls back once to the historic locations (dirs::cache_dir()/tokscale and ~/.cache/tokscale) when TOKSCALE_CONFIG_DIR is not set. Writes always land at the new path. Legacy files stay in place so a downgrade keeps working — no copy, no delete.

TUI renders cached data immediately, regardless of age (#471)

The 5-minute hard staleness cutoff previously gated the render decision on TUI launch: a cache older than that window blocked the user behind a full re-aggregation pass. Schema drift and filter-set mismatch still produce a Miss (rendering wrong-shape data is worse than showing nothing), but time-based aging no longer affects the render decision.

Fresh vs Stale distinction stays in CacheResult for future status-bar use ("data is N minutes old"), but the call site treats both as renderable. Background refresh runs unconditionally on TUI launch.

--write-cache flag and light.writeCache setting (#472)

CLI flags:

  • --write-cache (requires --light, conflicts_with --no-write-cache): atomically overwrite the TUI cache after --light renders, so the next tokscale tui launch starts from this report's data.
  • --no-write-cache: skip the cache write even if the setting opts in.

settings.json grows a light.writeCache: bool field (default false). #[serde(default)] keeps existing settings.json files loading unchanged — no migration needed.

Resolution: --no-write-cache > --write-cache > settings.light.writeCache > false. Cache write uses the existing atomic temp-file rename pattern, so a process crash mid-write never loses the cache.

Test results

Crate Tests
tokscale-core 576 unit + 10 codebuff + 3 hermes
tokscale-cli 441 unit + 84 integration
Total 1,114

Test count grew from 1,081 (pre-PR) → 1,114 (+33 new regression tests covering cache migration, TUI render-stale, --write-cache resolution matrix, schema/legacy fallback edges).

cargo check --workspace --release clean. cargo clippy no new warnings on changed code.

Backwards compatibility

Concern Handling
Old settings.json missing light field #[serde(default)] on Settings.light and LightSettings.write_cache
Legacy cache files at old paths Read-once fallback per file; no copy, no delete
macOS legacy split (Library/Caches AND .cache) Each cache module probes both legacy locations on macOS
Linux user with XDG_CONFIG_HOME set Already correct since #468; honored via dirs::config_dir()
TOKSCALE_CONFIG_DIR users (CI sandboxes) Override → no legacy fallback (hermeticity contract preserved)
Cache schema versions DON'T bump for path moves; TUI=6, core=7 unchanged
Lock files Move to new path; legacy lock at old path becomes irrelevant (transient anyway)
cli_tests.rs priming Added new path to the prime list so existing integration tests still find pricing data
Wrapped font/image cache Read-once fallback prevents network re-download on first run after upgrade
Downgrade-then-upgrade New version reads new path first; legacy file untouched so downgrade keeps working

Commits

  • `ff38d41` refactor(paths): hoist path helpers to tokscale-core and add get_cache_dir
  • `a5f5700` feat(cache): consolidate all caches under ~/.config/tokscale/cache with legacy fallback
  • `c5cfa21` feat(tui): render disk cache regardless of age and always background-refresh
  • `d346504` feat(cli,settings): add --write-cache flag and light.writeCache setting
  • `923a13a` docs: document unified cache layout, --write-cache flag, and light.writeCache setting

Each commit compiles and tests green standalone, so review is doable commit-by-commit.


Open in Devin Review

Summary by cubic

Unifies all regenerable caches under ~/.config/tokscale/cache, renders the TUI immediately from any valid cache while refreshing in the background, and adds an opt-in to write the TUI cache from --light runs. Path resolution now matches across reader/writer on Windows (Antigravity), and --light cache writes are best-effort and never change the exit code.

  • New Features

    • Single cache root at <config_dir>/cache (controlled by TOKSCALE_CONFIG_DIR) for TUI data, source-message, pricing, OpenCode migration, and Wrapped assets.
    • TUI renders cached data even if stale; background refresh always runs on launch.
    • --write-cache (requires --light) and --no-write-cache; settings.json adds light.writeCache (default false). Resolution: --no-write-cache > --write-cache > light.writeCache. Refuses to write when --since/--until/--year/--home are set. Writes are atomic and best-effort.
    • Antigravity cache at <config_dir>/antigravity-cache; Windows scanner now uses the same config root as the writer, so synced sessions appear in reports.
  • Migration

    • Read-side fallback to legacy paths (~/Library/Caches/tokscale, ~/.cache/tokscale) when TOKSCALE_CONFIG_DIR is not set; writes always go to <config_dir>/cache. Wrapped fonts/images are copied once from legacy into the new path on first use; other caches are not copied. Downgrade-safe.
    • Empty TOKSCALE_CONFIG_DIR is treated as unset; overrides remain hermetic (no legacy probing). Safe to delete <config_dir>/cache; schemas unchanged.
    • Test harness pins XDG_CONFIG_HOME and seeds the canonical cache path to keep cache resolution hermetic in CI (no runtime behavior change).

Written for commit f5bd599. Summary will update on new commits.

…e_dir

Tokscale-core's caches (source-message bincode, pricing JSON, the
OpenCode migration record) all need the same config dir resolution that
`Settings::load()` and `load_star_cache()` use, but the helpers lived
only in tokscale-cli. Move them to a new `tokscale_core::paths` module
and re-export them from the CLI side so existing call sites stay
untouched.

Adds two new helpers:

- `get_cache_dir()` returns `<config_dir>/cache`. The next commit
  consolidates every cache (TUI display data, source-message bincode,
  pricing JSON, opencode-migration.json, Wrapped fonts/images) under
  this single subdirectory so an isolated profile
  (`TOKSCALE_CONFIG_DIR=...`) covers everything in one shot.
- `legacy_dirs_cache_dir()` and `legacy_dot_cache_tokscale_dir()` resolve
  the historic `dirs::cache_dir()/tokscale` and
  `~/.cache/tokscale` locations respectively. Both probes return None
  when `TOKSCALE_CONFIG_DIR` is set so legacy data does not leak into
  isolated profiles. Both are needed on macOS because the historic split
  put the TUI display cache under `~/.cache/tokscale` and the source
  message / pricing caches under `~/Library/Caches/tokscale`.

No behavior change yet: all current callers still resolve the same
paths they did before. The next commit switches the cache modules over
and adds the legacy fallback chain.
…th legacy fallback

Tokscale state was scattered across up to four directories per platform.
On macOS specifically the source-message bincode and pricing JSON lived
under ~/Library/Caches/tokscale while the TUI display cache lived under
~/.cache/tokscale, and Wrapped image/font caches were in a third place.
The new defaultClients setting from #464 made this split visible: users
who set TOKSCALE_CONFIG_DIR for an isolated profile got a hermetic
config root but the caches still leaked from the host paths.

Consolidates every cache under <config_dir>/cache so:
- TOKSCALE_CONFIG_DIR controls everything in one shot (hermeticity)
- rm -rf ~/.config/tokscale/cache wipes only regenerable state
- The macOS-vs-Linux directory split is gone

Per-file moves:
- tui-data-cache.json: ~/.cache/tokscale/ -> <config_dir>/cache/
- source-message-cache.bin + .lock: dirs::cache_dir()/tokscale/ -> <config_dir>/cache/
- pricing-litellm.json + pricing-openrouter.json: same as above
- opencode-migration.json: same as above
- fonts/, images/ (Wrapped): ~/.cache/tokscale/<dir>/ -> <config_dir>/cache/<dir>/

Read-side migration is a one-time fallback per file: try the canonical
path first, then probe the legacy locations (gated off when
TOKSCALE_CONFIG_DIR is set). Writes always land at the new path. Legacy
files are left in place so a downgrade keeps working — no copy, no
delete.

Schemas stay at TUI=6, core=7. Path moves alone don't warrant a bump.

Closes #470
…refresh

The 5-minute cache staleness threshold previously gated the render decision on TUI launch: a Stale cache rendered with a background refresh, but a hard Miss (cache older than its hard-expiry window, schema drift, or filter-set mismatch) blocked the user behind a full re-aggregation pass. Schema drift and filter mismatch still produce a Miss because rendering wrong-shape or wrong-filter data is worse than showing nothing — but the time-based staleness check no longer affects the render decision.

The Fresh-vs-Stale distinction stays in CacheResult for future status-bar use (data age metadata), but the call site treats both as renderable. needs_background_load becomes unconditionally true on TUI launch so cached data always gets refreshed.

Closes #471
Adds an opt-in mechanism to warm the TUI cache from a --light run. Previously tokscale --light was strictly read-only — no way to opportunistically refresh the TUI cache from a CLI report even though the underlying UsageData was already computed. Daily-cron users would see a cold TUI on the next interactive launch.

CLI flags:
- --write-cache (requires --light): overwrite the TUI cache atomically after the report renders
- --no-write-cache (requires --light, conflicts with --write-cache): skip the cache write even if the setting opts in

Settings file:
- settings.json grows a light section with writeCache: false default
- #[serde(default)] keeps existing settings.json files loading unchanged

Resolution: --no-write-cache > --write-cache > settings.light.writeCache > false. Cache write uses the existing atomic temp-file rename, so a process crash mid-write never loses the cache.

Closes #472
…iteCache setting

- Cache layout: all regenerable caches now live under <config_dir>/cache. Documented for both posix and Windows in all four language READMEs.
- TOKSCALE_CONFIG_DIR row: clarifies the override now covers caches too.
- light.writeCache row: new opt-in setting documented in the configuration table.
- Cache directory layout subsection: explains what lives where and notes the directory is safe to delete (caches regenerate).
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
tokscale Ignored Ignored Preview Apr 26, 2026 8:14pm

Request Review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@junhoyeo junhoyeo changed the title feat: consolidate caches under ~/.config/tokscale/cache, render TUI cache eagerly, add --write-cache opt-in feat: consolidate caches under ~/.config/tokscale/cache, render TUI cache eagerly, add --write-cache opt-in Apr 26, 2026
@junhoyeo junhoyeo changed the title feat: consolidate caches under ~/.config/tokscale/cache, render TUI cache eagerly, add --write-cache opt-in feat: consolidate caches under ~/.config/tokscale/cache, render TUI cache eagerly, add --write-cache opt-in for light mode Apr 26, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

…ject empty TOKSCALE_CONFIG_DIR

The TUI cache key is `(enabled_clients, group_by)` only — it does NOT
include `--since`, `--until`, `--year`, or `--home`. `write_light_cache`
previously forwarded all of those into the DataLoader, so a
`tokscale --since 2025-01-01 --light --write-cache` invocation built a
date-filtered or home-scoped slice and saved it under the unfiltered
key. Subsequent `tokscale tui` launches would hit that cache and render
the filtered slice as if it were the default report — silent data
correctness loss flagged by both Devin and Codex review.

`write_light_cache` now refuses the write when any of those filters is
present and prints an eprintln explaining why. The CLI report still
prints normally; only the cache-warm side-effect is skipped.

Separately, `get_config_dir` previously treated an empty
`TOKSCALE_CONFIG_DIR=""` as a valid override and returned
`PathBuf::from("")`, while `is_config_dir_overridden` treated empty as
unset. After cache consolidation that mismatch could send writes to
relative paths like `./cache/...` even though the legacy fallback logic
still behaved as if no override was active. Resolver now agrees with
the predicate: empty string falls through to the platform default.
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…'t tank exit codes

The cache-warm step ran AFTER the report had already been flushed to
stdout, but propagated DataLoader errors via `?`. A scan failure
(e.g. a corrupt session file in one client's directory) would surface
as a non-zero exit code on a CLI invocation the user already saw print
correctly, breaking scripts and CI pipelines that key off exit status.

Changes write_light_cache return type from `Result<()>` to `()` and
swallows loader errors via `if let Ok(data) = loader.load(...)`,
matching the pattern in run_warm_tui_cache. The call site no longer
needs `?`. The eprintln-on-skip behavior (when --since/--until/--year/
--home are set) is unchanged.

Also fixes a stray clippy carryover in paths::tests where an empty
TOKSCALE_CONFIG_DIR fallback assertion compared a PathBuf against a
\&str literal.
Antigravity's scanner uses PathRoot::Config to resolve where to look
for synced sessions. Without this fix the resolver hardcoded
`{home_dir}/.config/tokscale` as the non-Linux fallback, while
get_antigravity_cache_dir() (the writer side) routes through
paths::get_config_dir() which calls dirs::config_dir() on Windows. The
two diverged on Windows: tokscale antigravity sync wrote to
%APPDATA%\\tokscale\\antigravity-cache\\ while the scanner read from
%USERPROFILE%\\.config\\tokscale\\antigravity-cache\\sessions\\, so
synced data silently never appeared in reports.

PathRoot::Config now mirrors get_config_dir's platform branches:
TOKSCALE_CONFIG_DIR override > Linux XDG_CONFIG_HOME > Windows
dirs::config_dir() > generic `{home}/.config/tokscale`. macOS still
falls through to `{home}/.config/tokscale` because get_config_dir()
deliberately overrides dirs::config_dir() there (which would otherwise
return ~/Library/Application Support/).

New regression test test_path_root_config_uses_dirs_config_dir_on_windows
locks the writer/scanner agreement on Windows.
…ng cache at canonical path

CI runners can have XDG_CONFIG_HOME set globally outside the sandboxed
HOME, so the post-#470 cache resolver leaks the binary's read+write
to the host's config dir. Three integration tests (test_monthly_json_offline_*,
test_submit_offline_*) flaked on Linux runners because the cost
expectations assumed the sandboxed pricing cache (or absence thereof),
but the binary was finding pricing data via the host's
$XDG_CONFIG_HOME/tokscale/cache/ — or worse, would write to it.

offline_cmd_with_home now sets XDG_CONFIG_HOME alongside the existing
XDG_CACHE_HOME and XDG_DATA_HOME pins, mirroring cmd_with_home and
keeping the binary's cache resolution fully inside the temp dir.

write_pricing_cache also seeds the canonical
.config/tokscale/cache/ directory so tests that exercise the new path
work even when legacy fallback is suppressed (e.g. via
TOKSCALE_CONFIG_DIR override).
…ermetic

CI runners that set XDG_CONFIG_HOME globally were leaking the binary's
canonical cache root outside the test's sandboxed HOME, so
SourceMessageCache::load read from the host's
$XDG_CONFIG_HOME/tokscale/cache/source-message-cache.bin (or wrote
there) instead of the temp dir. Three message_cache tests
(test_source_message_cache_round_trip,
load_falls_back_to_legacy_dirs_cache_path,
load_falls_back_to_legacy_dot_cache_path) and one pricing/cache test
(load_falls_back_to_legacy_dirs_cache_path) flaked on Linux runners as
a result.

Adds sandbox_cache_env / restore_cache_env helpers to message_cache so
the round-trip test pins HOME + XDG_CONFIG_HOME + XDG_CACHE_HOME
together. The two legacy-fallback tests gain explicit
XDG_CONFIG_HOME pinning to match. The pricing test does the same.
All 4 now pass on Linux CI.
@junhoyeo junhoyeo merged commit ff5f5d1 into main Apr 27, 2026
16 checks passed
@junhoyeo junhoyeo deleted the feat/unified-cache-dir branch April 27, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consolidate all caches under ~/.config/tokscale/cache/

1 participant